import os

import sys
import shutil
import re
import string
import sets
import codecs
import traceback

from util import trackstr, sanitizeFileName, ShaggerVersion, mypopen, parseTrackNum, extractIdFromUrl, GrokArtistFromPath

from config import Config

import musicbrainz2.disc as mbdisc
from musicbrainzdata import MBalbum, Lookup as MBLookup
from mutagen.flac import FLAC #for prepare destination, needs fixing.



class ProblemProcessingException(Exception):
    def __init__(self, value):
        self.value = value
    def __str__(self):
        return repr(self.value)

class GlobalStage:
    def __init__(self, shagger):
        self.shagger = shagger
        self.track = "whole album"
        if not self.shagger.isAShagger():
            raise ProblemHere
        self.practice = False
    
    def __call__(self):
        if( self.canWork()):
            self.shagger.log("performing %s." % (self.getFullPreamble()) )
            res = self.work()
            if not res:
                raise ProblemProcessingException, "Couldn't work %s" % self.getFullPreamble()
            return True
        else:
            self.shagger.log("no need to perform %s." % (self.getFullPreamble()))
            return True
 
    def getFullPreamble(self):
        return self.preamble()
    
    def setPracticeRun(self, arg):
        self.practice = arg
    
    def filename(self, file):
        return self.shagger.filename(file)
    
    #Implement these...
    
    def preamble(self):
        return "Unamed operation"
    
    def canWork(self):
        raise NotImplementedError

    def work(self):
        raise NotImplementedError

class Shagstage(GlobalStage):    
    def __init__(self, shagger, track):
        GlobalStage.__init__(self, shagger)        
        self.track = track
    
    def getFullPreamble(self):
        return "%s on track %d" % (self.preamble(), self.track)
                
    def filename(self, suffix):
        return self.shagger.trackfilename(self.track, suffix)
    
    def touch(self, suffix):
        file = self.filename(suffix)
        self.shagger.touch(file)
    
    def fetchFiles(self):
        list = os.listdir(self.shagger.dir())
        out = []
        for file in list:
            if file.startswith(trackstr(self.track)):
               #print("Found %s" % file)
                out = out + [ file ]
        return out
    
    def log(self, msg):
        self.shagger.log(msg + "(%02d)" % self.track )


class FetchCDId(GlobalStage):
    #this stage requires a CD and could be skipped if you find an alternative way of setting the discid
    
    file = Config['cdidfile']
        
    def __init__(self, shagger):
        GlobalStage.__init__(self, shagger) 
    
    def preamble(self):
        return "Read CD Id"
    
    def canWork(self):       
        return True
    
    def work(self):
        disc = mbdisc.readDisc( Config['cddevice'] )
        self.shagger.discid = disc.getId() #the _really_ imporant bit of this stage. 
        self.shagger.tracks = range(disc.getFirstTrackNum(), disc.getLastTrackNum()+1)
        
        self.shagger.log("Working in %s" % (self.shagger.dir()) )
        
        urlfile = os.path.join(self.shagger.dir(), self.file)
        url = mbdisc.getSubmissionUrl(disc)
        
        f = open(urlfile, "w")
        f.write(url)
        f.close()
        
        return True

class LocalCDId(GlobalStage):
    #an alternative to the above that doesn't require a disc.
    def __init__(self, shagger, id):
        GlobalStage.__init__(self, shagger)
        self.id = id
    
    def preamble(self):
        return "Setting disc id to %s" % self.id
    
    def canWork(self):       
        return True
    
    def work(self):
        self.shagger.discid = self.id
        
        #now work out the tracks...
        files = self.shagger.fileSubset("")
        tracks = []
               
        
        for file in files:
            track = parseTrackNum(file)
            if( track != None):
                tracks = tracks + [track]
        
        #make unique
        tracks = list(sets.Set(tracks)) #remove duplicates
        tracks.sort()
        
        self.shagger.log("Discid is %s" % self.id)        
        self.shagger.log("Tracks are %s" % tracks)
        
        
        self.shagger.tracks = tracks
            
        return True

class ImplyDiscID(GlobalStage):
    #This variant reads the discID from any track.
    hintfile = "musicbrain"
    flacfile = ""
    
    def __init__(self, shagger):
        GlobalStage.__init__(self, shagger)
        #self.dir = dir
    
    def preamble(self):
        return "Working dir (DiscID implied) %s" % self.shagger.absdir
    
    def canWork(self):
        files = self.shagger.fileSubset("flac")
        if len(files) > 0:
            flacfile = files[0]
            return True
        cues = self.shagger.fileSubset( "cue" )
        if( len( cues ) == 1):
            return True
        self.shagger.log( "No music files" )
        return False
    
    def work(self):
        #self.shagger.absdir = self.dir
        tracks = []                
        self.shagger.tracks = tracks
        
        if( self.flacfile ):
            audio = FLAC( self.flacfile)
            if( audio.has_key("musicbrainz_discid")):
                id = audio["musicbrainz_discid"][0];        
                self.shagger.log("Implied discid of %s\n" % id)
                self.shagger.discid = id
                return True
            elif( audio.has_key("musicbrainz_albumid")):
                id = audio["musicbrainz_albumid"][0];
                self.shagger.log("Implied albumid of %s\n" % id)
                self.shagger.discid = id
                return True
        
        #maybe there's a hint file?
        print("joining %s to %s" % (self.shagger.dir(), self.hintfile))
        file = os.path.join(self.shagger.dir(), self.hintfile)
        if(os.path.exists(file)):
            f = codecs.open(file, "r", encoding="utf-8")
            id = string.rstrip(f.read())
            f.close()
            #maybe the lazy guy just pasted a URL into the file?
            id = extractIdFromUrl(id)
            self.shagger.log("Implied albumid of %s from hintfile %s\n" % (id, self.hintfile))
            self.shagger.discid = id
            return True
        else:
            self.shagger.log("Couldn't imply musicbrainz id from tag")
            return False                

class LookupDiscId(GlobalStage):
    hintfile = ImplyDiscID.hintfile
    missfile = ImplyDiscID.hintfile + "x"
    threshold = 99
    flacbased = 0
    grokFile = ""
    grokFileName = ""
    trackcount = 0
    
    def __init__(self, shagger):
        GlobalStage.__init__(self, shagger)
        #self.dir = dir
    
    def preamble(self):
        return "Lookup DiscId %s" % self.shagger.absdir
    
    def canWork(self):
        file = os.path.join(self.shagger.dir(), self.hintfile)
        if(os.path.exists(file)):
            self.shagger.log( "Found discid: %s" % file )
            return False
        flacs = self.shagger.fileSubset("flac")
        if len(flacs) > 0:
            self.grokFile = flacs[0]
            self.trackcount = len( flacs )
            return True 
        
        cues = self.shagger.fileSubset("cue")
        if len(cues) == 1:
            #Fixme: Would be neat if we could extract # tracks at this point
            grokFileName = cues[0]
            return True         
        self.shagger.log( "Not found any music files" )
        return False
    
    def work(self):
        artist = None
        album = None
                
        #so first, is there a tag in the files?
        if( self.grokFile ):        
            audio = FLAC(self.grokFile)
            if( audio.has_key("musicbrainz_discid")):
                return True #no need to hint.
            if( audio.has_key("artist") and audio.has_key("album")):
                #TODO: need to sort out various-artist
                artist = audio["artist"][0]
                album = audio["album"][0]

        if( self.grokFileName ):
            (artist, album) = GrokArtistFromPath(self.grokFile)
                
        if( ( artist == None ) or ( album == None ) ):
            (artist, album) = GrokArtistFromPath(self.shagger.dir())        
        
        if( artist == None or album == None):
            self.shagger.log("I have no idea what the album (%s) or artist (%s) is. Sorry")
            return False
        
        hits = []
        results = MBLookup(artist, album)
        for result in results:
            if(result.getScore() < self.threshold):
                continue
            if( self.grokFile and result.getRelease().getTracksCount() != self.trackcount):
                continue
            
            hits = hits + [result]
        
        ret = None
        
        if( len(hits) > 1):
            print("Lots of hits:")
            hits = results
            ret = False
        elif( len(hits) == 0):
            print("no hits:")
            hits = results
            ret = False        
        else:
            ret = True
        
        if(ret):
            file = os.path.join(self.shagger.dir(), self.hintfile)
        else:
            file = os.path.join(self.shagger.dir(), self.missfile)
        
        f = codecs.open(file, "w", encoding="utf-8")
        for hit in hits:            
            r = hit.getRelease()
            f.write("%s %s-%s (tracks: %d) (score: %d)\n" % 
                    (r.getId(), r.getArtist().getName(), r.getTitle(), r.getTracksCount(), hit.getScore())
                    )       
        f.close()
                
        return ret
        
        
        

class Beepstage(GlobalStage):
    ok = 100
    error = 101
    done = 102
    fail = 103
    
    def __init__(self, shagger, beep):
        GlobalStage.__init__(self, shagger) 
        self.beep = beep
    
    def preamble(self):
        return "Fetch musicbrainz data"
    
    def canWork(self):       
        return True   
    
    def work(self):
        if self.beep == self.ok:
            os.system("beep -f 440")
            os.system("beep -f 880")
        elif self.beep == self.error:
            os.system("beep -f 880")
            os.system("beep -f 440")
        elif self.beep == self.done:
            os.system("beep -f 440")
            os.system("beep -f 880")
            os.system("beep -f 880 -l 600")
        elif self.beep == self.fail:
            os.system("beep -f 880")
            os.system("beep -f 440")
            os.system("beep -f 440 -l 600")
        else:
            return False
        
        return True
        
class FetchMusicbrainzData(GlobalStage):
    #This stage requires t'internet.    
        
    def __init__(self, shagger):
        GlobalStage.__init__(self, shagger) 
    
    def preamble(self):
        return "Fetch musicbrainz data"
    
    def canWork(self):       
        return True
    
    def work(self):
        #urlfile = os.path.join(self.shagger.dir(), self.file)
        #url = mbdisc.getSubmissionUrl(shagger.disc)
        mb = MBalbum()
        if(len(self.shagger.discid) == 28):                
            try:
                mb.LoadFromDisc(self.shagger.discid)
            except Exception, details:
                #traceback.print_exc()
                self.shagger.log("Can't load musicbrainz data\n%s" % details)        
                return False
        elif(len(self.shagger.discid) == 36):
            try:
                mb.LoadFromReleaseId(self.shagger.discid)                 
            except Exception, details:
                #traceback.print_exc()
                self.shagger.log("Can't load musicbrainz data (album %s)\n%s" % (self.shagger.discid, details))   
                return False   
        else:
            self.shagger.log("Shagger has no valid discid")
            return False
        
        self.shagger.musicBrain = mb
        self.shagger.tracks = mb.getTracks().keys()
        self.shagger.log("Shagger musicbrain is %s" % mb )
        return True

class Initial(Shagstage):
    
    suffix = "todo"
    
    def __init__(self, shagger, track):
        Shagstage.__init__(self, shagger, track)
 
    def preamble(self):
        return "Intial"
    
    def canWork(self):
        if( len(self.fetchFiles()) == 0):
            return True
        return False
    
    def work(self):
        if self.canWork():
            self.log("New track")
            f = open(self.filename(self.suffix), "w")
            f.close()
            return True

        

class Rip(Shagstage):
    
    previous = Initial.suffix
    working = "ripping.wav"
    suffix = "cdda.wav"
    
    def __init__(self, shagger, track):
        Shagstage.__init__(self, shagger, track)        
    
    def preamble(self):
        return "Ripper"
    
    def canWork(self):
        files = self.fetchFiles()
        for file in files:
            if file.endswith(self.previous):
                return True    
            
    def work(self):
        if not self.canWork():
            return False
        
        self.log ("Ripping track:")
        cdcommand = 'cdparanoia'
        
        outsuffix = os.path.join(self.shagger.dir(), "ripping.wav" )
        newoutfile = self.filename( self.suffix )
        
        #NB -z makes it read forever - the default 20 retries is probably sufficient for most needs!
        args =   [ "-X", "-B", "%d"% self.track, outsuffix ] #-q for quiet?
        proc = mypopen([cdcommand] + args) #.communicate()[0]
        (pid, out) = os.waitpid( proc.pid, 0 )
        
        #print >> sys.stderr, "Child returned", (pid, out)
        
        if(out == 0):
            outfile = self.filename(self.working)
            
            self.log("Renaming %s to %s" % (outfile, newoutfile))
            os.rename(outfile, newoutfile)
            os.remove(self.filename(self.previous))
            self.log("Ripped track to %s:" % newoutfile )
            return True  
        else:
            raise ProblemProcessingException, "ripping error %d" % out
    

class Eject(GlobalStage):
    
    def __init__(self, shagger):
        GlobalStage.__init__(self, shagger) 
    
    def preamble(self):
        return "eject"
    
    def canWork(self):        
        return True
    
    def work(self):
        os.system("eject &")
        return True
 
        
